# Author: Allen Hutchison
# Copyright (C) 2006 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#      Unless required by applicable law or agreed to in writing, software
#      distributed under the License is distributed on an "AS IS" BASIS,
#      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#      See the License for the specific language governing permissions and
#      limitations under the License.
#
# Config package for the Open Automation Framework. This module is responsible
# for finding and parsing config files. It is also responsible for storing
# config information which can be retrieved in a test script.

# TODO: Add universal command line parsing for any config value.
#       --oaf.email=foo@foo.com for example, some done but need
#       to refine so that we can still feed args to a script.

package Oaf::Config;
use base qw(Oaf);

use strict;
use warnings;

use FindBin qw($Bin);
use File::Copy;
use File::Temp qw(tempfile);
use File::stat;
use File::Spec;
use File::Basename;

# new()
#  Create a new instance of an Oaf::Config.
#  This is used by the new() method in Oaf.
sub new {
  my ($class, %params) = @_;

  my $self = {};
  bless $self, $class;

  $self->read_config($self->find_config(\%params));

  foreach (keys (%params)) {
    my $val = $params{$_};
    my ($sec, $var) = split /\./, $_;
    $self->set_param($sec, $var, $val);
  }

  $self->parse_command_line();

  return $self;
}

# read_config()
#  Config files look like regular ini files, this method
#  reads the config file and parses it into a data structure
#  that is stored in the object.
sub read_config {
  my ($self, $config_file) = @_;

  unless(defined($config_file)) {
    $config_file = $self->find_config();
  }

  if (-e $config_file) {

    # First, check to see if we have stored the file mtime already
    my $last_mod = $self->_get_param('oaf', 'config_file_mtime', 0);

    # Get the real mtime from the file
    my $st = stat($config_file);

    # If the file has been written to since we last read it
    # read it again.
    if ($last_mod < $st->mtime) {
      open(CONFIG, $config_file);
      my $sec;
      while (<CONFIG>) {
	# Process the lines from the file
	chomp();   # Remove \n
	s/^\s+//;  # Sub out leading spaces
	s/\s+$//;  # Sub out lines full of blanks
	s/#.*//;   # Sub out comment lines
	
	# Skip Blank Lines, possibly created above.
	next if $_ eq '';
	
	# Sections are defied by: [section_name]
	if(/^\[(\S+)\]$/) {
	  $sec = $1;
	}

	# Values are defined by: var = val
	if(/(\S+)\s*=\s*(\S+)/) {
	  if (defined $sec) {
	    $self->_set_param($sec, $1, $2);
	  } else {
	    $self->error("Invalid entry in config file: $_");
	  }
	}
      }
      close(CONFIG);
      # Store the mtime for the file we just read.
      $self->_set_param('oaf', 'config_file_mtime', $st->mtime);
    }
  }
}

# write_config()
#  Writes an OAF config file representing the data in the
#  current config object.
sub write_config {
  my ($self, $config_file) = @_;

  unless(defined($config_file)) {
    $config_file = $self->find_config();
  }

  if (-e $config_file) {

    # Write to a temp file instead of using flock so that
    # we don't break over NFS.
    my $base_path = $self->_get_param('oaf', 'base_path');
    my($tmp_fh, $tmp_filename) = tempfile(DIR => File::Spec->catfile($base_path, 'tmp'));

    foreach my $sec ($self->get_param_sections()) {
      print $tmp_fh "[$sec]\n";
      foreach my $var ($self->get_param_vars($sec)) {
	my $val = $self->_get_param($sec, $var);
	print $tmp_fh "$var = $val\n";
      }
    }
    close($tmp_fh);
    # Rename the tmp file to the real file because
    # flock is broken over nfs.
    move $tmp_filename, $config_file;
    $self->_set_param('oaf', 'config_file_mtime', 0);
  }
}

# parse_command_line()
#  TODO: Needs to be rewritten to leave normal args in the ARGV stack
sub parse_command_line {
  my($self) = @_;

  foreach (map { [split /=/, substr $_, 2] } grep { /^--\S+=\S+/ } @ARGV) {
    # $_ contains [ key, value ] array ref tuples
    $_->[0] =~ /(\w+)\.(\w+)/;
    $self->set_param($1, $2, $_->[1]);
  }

}


# set_param()
#  This routine is used to set a parameter in
#  the config data structure in the object.
sub set_param {
  my($self, @args) = @_;

  unless (scalar(@args) == 3) {
    $self->error("set_param should be called with 3 arguments: @args");
  }

  my $value = $self->_set_param(@args);

  if (defined($self->{'config_file'})) {
    $self->write_config();
  }
  return $value;
}

# _set_param()
#  This is the method that actually manipulates the config
#  data structure. It's used by Oaf::Config only when the
#  added write_config isn't advised.
sub _set_param {
  my($self, $sec, $param, $value) = @_;

  $sec = lc($sec);
  $param = lc($param);


  $sec = $self->_alias($sec);

  $self->{'params'}->{$sec}->{$param} = $value;

  return $value;

}

# get_param()
#  This routine is used to get a parameter in
#  the config data structure in the object.
sub get_param {
  my($self, @args) = @_;

  # If we are bootstrapping the config
  # then config_file won't be defined and
  # we don't want to try to refresh the object.
  if (defined($self->{'config_file'})) {
    $self->read_config();
  }

  unless (scalar(@args) >= 2) {
    $self->error("get_param should be called with 2 or more arguments: @args");
  }

  return $self->_get_param(@args);
}

# _get_param()
#  This is the method that actually manipulates the config
#  data structure. It's used by Oaf::Config only when the
#  added write_config isn't advised.
sub _get_param {
  my($self, $sec, $param, $value) = @_;

  $sec = lc($sec);
  $param = lc($param);


  $sec = $self->_alias($sec);

  if (defined($self->{'params'}->{$sec}->{$param})) {
    return $self->{'params'}->{$sec}->{$param};
  } elsif (defined $value) {
    return $self->_set_param($sec, $param, $value)
  } else {
    return undef;
  }
}

# get_param_sections()
#  Get all of the sections from the config object
sub get_param_sections {
  my($self) = @_;
  return keys(%{$self->{'params'}});
}

# get_param_vars()
#  Get all of the vars in a section of a config object
sub get_param_vars {
  my($self, $sec) = @_;
  return keys(%{$self->{'params'}->{$sec}});
}

# _alias()
#  Oaf supports a special config section called an alias
#  this allows you to use generic names for command line
#  options, and still have them resolve to specific devices
#  An alias can be n levels deep, and alias() checks for loops.
sub _alias {
  my($self, $sec) = @_;

  my %seen;
  my $latest = $sec;

  $seen{$sec} = 1;

  while($sec = $self->{'params'}->{':aliases:'}->{$sec}) {
    if(exists $seen{$sec}) {
      $self->warning("There is a circular reference in your alias section: $sec");
      last;
    } else {
      $seen{$sec} = 1;
      $latest = $sec;
    }
  }
  return $latest;
}

# find_base_path()
#  Find the base directory for this instance of Oaf. In the future
#  OAF will have two install options, as a system module or as a downloaded
#  package (the current scheme). This should work in both instances.
sub find_base_path {
  my($self, $params) = @_;

  # The command line takes precedence
  my $base_path = $params->{'oaf.base_path'};
  unless (defined($base_path)) {

    # If base isn't defined on the command line
    # Check if it's in the config object
    if (defined $self->_get_param('oaf', 'base_path')) {
      $base_path = $self->_get_param('oaf', 'base_path');

      # If it's not in the config object
      # Check to see if it's in the environment
    } elsif (defined($ENV{'OAF'})) {
      $base_path = $ENV{'OAF'};

      # If all else fails, see if we can figure
      # out  where the base is. Check for all the
      # directories we put in the oaf base to avoid
      # false positives.
    } else {
        until ((-e File::Spec->catfile($Bin, 'bin'))     and
               (-e File::Spec->catfile($Bin, 'conf')) and
               (-e File::Spec->catfile($Bin, 'lib'))     and
	           (-e File::Spec->catfile($Bin, 'logs'))    and
	           (-e File::Spec->catfile($Bin, 'queue'))   and
	           (-e File::Spec->catfile($Bin, 'scripts')) and
	           (-e File::Spec->catfile($Bin, 'suite'))   and
	           (-e File::Spec->catfile($Bin, 'tmp'))) {
	               $Bin = dirname($Bin);
	               # If we have cut down Bin until it's just root
	               # then we aren't going to find the base_path.
	               if ($Bin eq File::Spec->rootdir()) {
	                   $self->fatal_error("I can't find the base_path");
	               }   
               }
        $base_path = $Bin;
    }
  }
  # Trim any cruft off the path.
  $base_path = File::Spec->canonpath($base_path);
  return $self->_set_param('oaf', 'base_path', $base_path);
}


# find_config()
#  Find the config file for this instance of Oaf. This can be
#  complicated because different config objects share the same
#  config file. If this is the first instance of an oaf config
#  for this run, then we'll have to create the tmp_config_file
#  that all other instances will use.
sub find_config {
  my ($self, $params) = @_;

  # If we have already done this work before
  # don't do it again.
  if (defined($self->{'config_file'})) {
    return $self->{'config_file'};
  }

  # Figure out the path for the tmp_config_file.
  my $base_path = $self->find_base_path($params);
  my $pgrp = getpgrp();
  my $tmp_config_file = File::Spec->catfile($base_path, 'tmp', "config.ini.$pgrp");
  my $config_file = File::Spec->catfile($base_path, 'tmp', 'config.ini');
  
  # If our tmp_config_file already exists, then there is another
  # instance of a config object responsible for it. We just read
  # it.
  if (-e $tmp_config_file) {
    $self->debug("Config file $tmp_config_file found");
    $self->{'i_own_config_file'} = 0;

    # If we didn't find a $tmp_config_file, then we are going to
    # look for a config file from the arguments (either command line
    # or passed in).
  } elsif ((defined($params->{'oaf.config_file'})) and
	   (-e $params->{'oaf.config_file'})){
    $self->debug("Copying $params->{'oaf.config_file'} to $tmp_config_file");
    copy($params->{'oaf.config_file'}, $tmp_config_file);
    $params->{'oaf.config_file'} = $tmp_config_file;
    $self->{'i_own_config_file'} = 1;

    # If there was no special config file specified, we try the default
    # config file.
  } elsif(-e $config_file) {
    $self->debug("Copying $config_file to $tmp_config_file");
    copy("$config_file", "$tmp_config_file");
    $self->{'i_own_config_file'} = 1;
  }

  if (-e $tmp_config_file) {
    $self->{'config_file'} = $tmp_config_file;
    return $tmp_config_file;
  } else {
    return undef;
  }
}

# DESTROY()
#  When the config object is destroyed we have to check if this instance
#  owns the tmp config file. If so, then we have to clean it up. See
#  find_config() in this file for more details.
sub DESTROY {
  my($self) = @_;
  my $config = $self->{'config_file'};

  if (1 == $self->{'i_own_config_file'}) {
    $self->debug("I own config file $config");
    unlink($self->{'config_file'});
  } else {
    $self->debug("I don't own config file $config");
  }
}

# Have to eval to something
1;
__END__
